package org.third.suite.service.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.function.Function;

import javax.persistence.Entity;
import javax.sql.DataSource;

import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItem;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.third.common.domain.Deployment;
import org.third.common.utils.CertUtil;
import org.third.common.utils.EnvUtils;
import org.third.common.utils.K8S_OPERATION_UTIL;
import org.third.common.utils.LambdaExceptionUtil;
import org.third.common.utils.PathBuilder;
import org.third.common.utils.StreamUtils;
import org.third.common.utils.VaultAndKubeRequestUtil;
import org.third.integration.kubernates.cmd.imp.api.K8sApiBuilder;
import org.third.integration.vault.VaultBinClient;
import org.third.suite.service.InstallerDataSourceService;

/**
 * Created by Huailong Tang(Jason) on 2017/10/16.
 */
@Service
public class InstallerDataSourceServiceImp implements InstallerDataSourceService {

	@Autowired
	private Map<String, DataSource> installerDataSource;

	@Autowired
	VaultAndKubeRequestUtil vaultAndKubeRequestUtil;

	@Autowired
	K8sApiBuilder k8sApiBuilder;

	@Autowired
	private DataSourceProperties dataSourceProperties;

	@Autowired
	private VaultBinClient vaultBinClient;

	@Value("${RETRY_TIMES:360}")
	private Long dbRetryExpiration;

	private String uuidStr = UUID.randomUUID().toString();

	private final String databasePasswordKey = "suitedb_user_password";

	private String databasePassword = "";

	private static final Logger logger = LoggerFactory.getLogger(InstallerDataSourceServiceImp.class);

	@Override
	public void initialize() {
		synchronized (this) {
			logger.info("postgres initialize ...");
			MetadataSources metadata = new MetadataSources(new StandardServiceRegistryBuilder()
					.applySetting(AvailableSettings.DIALECT, "org.hibernate.dialect.PostgreSQLDialect")
					.applySetting(AvailableSettings.DRIVER, dataSourceProperties.getDriverClassName())
					.applySetting(AvailableSettings.URL, dataSourceProperties.getUrl())
					.applySetting(AvailableSettings.USER, dataSourceProperties.getUsername())
					.applySetting(AvailableSettings.PASS, initDataBasePassword()).build());

			ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(
					false);
			scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
			// use Deployment's package as basePackage
			for (BeanDefinition bd : scanner.findCandidateComponents(Deployment.class.getPackage().getName())) {
				String name = bd.getBeanClassName();
				try {
					metadata.addAnnotatedClassName(name);
				} catch (Exception e) {
					logger.error("get database entity fail ...");
					e.printStackTrace();
				}
			}
			// Don't use SchemaExport (equal = create drop) ,ShemaUpdate used for
			// update
			SchemaUpdate schemaUpdate = new SchemaUpdate((MetadataImplementor) metadata.buildMetadata());
			schemaUpdate.execute(true, true);
		}
	}

	@Override
	public void addNewDataSourceAtRuntime() {
		synchronized (this) {
			logger.info("add new datasource at runtime ...");
			if (!installerDataSource.containsKey("DEFAULT")) {
				logger.info("add DEFAULT datasource");
				org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
				dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
				dataSource.setUrl(dataSourceProperties.getUrl());
				dataSource.setUsername(dataSourceProperties.getUsername());
				dataSource.setPassword(initDataBasePassword());
				dataSource.setInitialSize(10);
				dataSource.setTestWhileIdle(true);
				dataSource.setTestOnBorrow(true);
				dataSource.setTimeBetweenEvictionRunsMillis(30000);
				dataSource.setValidationQuery("select 1");
				installerDataSource.put("DEFAULT", dataSource);
			}
		}
	}

	@Override
	public String getDataBaseKey() {
		logger.info("obtain postgres password key  ...");
		initDataBasePassword();
		return databasePasswordKey;
	}

	@Override
	public synchronized boolean databaseReady() {
		return pgDatabaseReadyWithSecure(dataSourceProperties.getUrl(), dataSourceProperties.getUsername(),
				initDataBasePassword(), false, (connection -> {
					return true;
				})).isAbleConn();
	}

	@Override
	public boolean databaseReady(DataBaseDTO dataBaseDTO) {
		boolean isReady = false;
		switch (dataBaseDTO.getType()) {
		case SS_CONSTANT.POSTGRES:
			isReady = pgDataBaseReadyWithDTO(dataBaseDTO);
			break;
		case SS_CONSTANT.ORACLE:
			isReady = oracleDatabaseReadyWithDTO(dataBaseDTO);
			break;
		default:
			isReady = true;
			break;
		}
		return isReady;
	}

	@Override
	public void uploadJdbcConnCertificate(String type, MultipartFile... multipartFiles) throws IOException {
		if (StringUtils.isEmpty(type)) {
			throw new IllegalArgumentException("type is empty or null");
		}
		if (multipartFiles.length == 0) {
			throw new IllegalArgumentException("not upload any file");
		}
		if (multipartFiles.length > 0) {
			remoteAllPreviousFiles(type);
			uploadCertificates(type, multipartFiles);
		}
	}

	@Override
	public MultipartFile convertToMultifile(String path) throws IOException {
		Path path1 = Paths.get(path);
		File file = new File(path);
		FileInputStream input = new FileInputStream(file);
		FileItem fileItem = new DiskFileItem("file", "text/plain", true, file.getName(), 200000, file.getParentFile());
		// MultipartFile multipartFile = new CommonsMultipartFile(fileItem);
		// input.close();
		MultipartFile multipartFile = null;
		return multipartFile;
	}

	@Override
	public boolean databaseReadyWithRetry() {
		boolean isValid = false;
		Connection conn = null;
		long timeExpanded = 0;
		while (isValid == false && timeExpanded < dbRetryExpiration) {
			try {
				timeExpanded = timeExpanded + 5;
				conn = DriverManager.getConnection(dataSourceProperties.getUrl(), dataSourceProperties.getUsername(),
						initDataBasePassword());
				isValid = conn.isValid(5);
			} catch (SQLException ex) {
				logger.error("connect to postgres fail ...");
				ex.printStackTrace();
				try {
					Thread.sleep(5000);
				} catch (InterruptedException th) {

				}
			} finally {
				if (conn != null) {
					try {
						conn.close();
					} catch (SQLException sx) {

					}
				}
			}
		}

		logger.info("postsql database status is {}", isValid);
		return isValid;
	}

	@Override
	public boolean databaseExistInKube() {
		String apiServerToken = "";
		try {
			apiServerToken = StreamUtils.getFileContent(EnvUtils.getApiservertokenfile());
			if (!StringUtils.isEmpty(apiServerToken)) {
				String getResUrl = k8sApiBuilder.buildAPI("core", K8S_OPERATION_UTIL.K8S_API_1_6.get("deployments"),
						"deployments", "suite-db");
				ResponseEntity responseEntity = vaultAndKubeRequestUtil.callRequestToKube(HttpMethod.GET, getResUrl,
						apiServerToken, null, String.class);
				if (vaultAndKubeRequestUtil.isHttpSuccessful(responseEntity.getStatusCode())) {
					logger.info("find suite-db deployment ");
					return true;
				}
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return false;
	}

	@Override
	public String getDatabaseName() {
		String defaultName = "suitedb";
		if (!StringUtils.isEmpty(dataSourceProperties.getUrl())) {
			try {
				URI uri = URI.create(dataSourceProperties.getUrl().substring(5));
				defaultName = uri.getRawPath().replace("/", "");
				logger.info("get database name {} from jdbc url", defaultName);
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
		return defaultName;
	}

	@Override
	public boolean existeDataSource() {
		return installerDataSource.containsKey("DEFAULT");
	}

	private String initDataBasePassword() {
		if (StringUtils.isEmpty(this.databasePassword)) {
			try {
				this.databasePassword = vaultBinClient.getSecret(this.databasePasswordKey);
			} catch (Exception e) {
				logger.error("get exist postgres password from vault fail ...");
				e.printStackTrace();
			}
			if (StringUtils.isEmpty(databasePassword)) {
				String rawPassword = UUID.randomUUID().toString();
				String role = System.getenv("APPROLE");
				String roleID = System.getenv("ROLE_ID");
				String rawNamespace = System.getenv("SYSTEM_NAMESPACE");
				try {
					vaultBinClient.storeSecret(rawNamespace, role, roleID, this.databasePasswordKey, rawPassword);
					this.databasePassword = rawPassword;
				} catch (Exception e) {
					e.printStackTrace();
					logger.error("save postgres password to vault fail ...");
				}
			}
		}
		return this.databasePassword;
	}

	private boolean pgDataBaseReadyWithDTO(DataBaseDTO dataBaseDTO) {
		ConnDBInfo connDBInfo = pgDatabaseReadyWithSecure(buildPostgresConnUrl(dataBaseDTO),
				dataBaseDTO.getParam().getDb_user(), dataBaseDTO.getParam().getDb_password(),
				dataBaseDTO.getParam().isSecure(), (connection -> {
					return true;
				}));

		final String dbName = dataBaseDTO.getParam().getDb_name();
		// 3D0000 meaning that invalid_catalog_name,
		// https://www.postgresql.org/docs/current/static/errcodes-appendix.html
		if (dataBaseDTO.getParam().isDb_autoCreate() && !connDBInfo.isAbleConn()
				&& connDBInfo.getConnMessage().equals("3D000")) {
			logger.info("need to create database for suite {}", connDBInfo);
			// set default database name for connect , evoid to connect to template1
			dataBaseDTO.getParam().setDb_name("postgres");
			connDBInfo = pgDatabaseReadyWithSecure(buildPostgresConnUrl(dataBaseDTO),
					dataBaseDTO.getParam().getDb_user(), dataBaseDTO.getParam().getDb_password(),
					dataBaseDTO.getParam().isSecure(), connection -> {
						try (Statement statement = connection.createStatement()) {
							statement.executeUpdate("CREATE DATABASE " + dbName);
						} catch (SQLException ex) {
							ex.printStackTrace();
							return false;
						}
						return true;
					});
		}
		return connDBInfo.isAbleConn();
	}

	private boolean oracleDatabaseReadyWithDTO(DataBaseDTO dataBaseDTO) {
		return oracleDatabaseReady(buildOracleConnUrl(dataBaseDTO), dataBaseDTO.getParam().getDb_user(),
				dataBaseDTO.getParam().getDb_password(), dataBaseDTO.getParam().isSecure());
	}

	private ConnDBInfo pgDatabaseReadyWithSecure(String url, String username, String password, boolean isSecure,
			Function<Connection, Boolean> createMethod) {
		ConnDBInfo connDBInfo = new ConnDBInfo();
		Connection conn = null;
		Properties info = new Properties();
		if (isSecure == true) {
			info.setProperty("ssl", "true");
			String crtContents = CertUtil.readCertificateContent(SS_CONSTANT.JDBC_CONF_CRT_PATH);
			if (!StringUtils.isEmpty(crtContents)) {
				info.setProperty("sslfactory", "suiteinstaller.security.cert.SingleCertValidatingFactory");
				info.setProperty("sslfactoryarg", CertUtil.readCertificateContent(SS_CONSTANT.JDBC_CONF_CRT_PATH));
			}
		}
		info.setProperty("user", username);
		info.setProperty("password", password);
		try {
			conn = DriverManager.getConnection(url, info);
			connDBInfo.setAbleConn(conn.isValid(10));
			if (!createMethod.apply(conn)) {
				connDBInfo.setAbleConn(false);
			}
		} catch (SQLException ex) {
			logger.error("connect to postgres fail ...");
			connDBInfo.setAbleConn(false);
			connDBInfo.setConnMessage(ex.getSQLState());
			ex.printStackTrace();
		} finally {
			if (conn != null) {
				try {
					conn.close();
				} catch (SQLException e) {

				}
			}
		}
		logger.info("postsql database  status is {}", connDBInfo.isAbleConn());
		return connDBInfo;
	}

	private boolean oracleDatabaseReady(String url, String user, String password, boolean isSecure) {
		Connection conn = null;
		boolean isValid = false;
		String driverPath = new PathBuilder(SS_CONSTANT.JDBC_CONF_JAR_PATH).buildUnixPath();
		File driverFile = new File(driverPath);
		if (driverFile.exists() && driverFile.isDirectory()) {
			try {
				URL[] urls = Arrays.stream(driverFile.listFiles())
						.filter(singleFile -> singleFile.getName().startsWith("oracle"))
						.map(LambdaExceptionUtil.rethrowFunction(
								single -> new URL("jar", "", "file:" + single.getAbsolutePath() + "!/")))
						.toArray(URL[]::new);
				Driver driver = loadDriverFromExternalJar(urls);
				Properties properties = new Properties();
				properties.setProperty("user", user);
				properties.setProperty("password", password);
				if (isSecure) {
					File tmpFile = CertUtil.convertCrtToJks(new File(SS_CONSTANT.JDBC_CONF_CRT_PATH));
					properties.setProperty("javax.net.ssl.trustStore", tmpFile.getAbsolutePath());
					properties.setProperty("javax.net.ssl.trustStoreType", "JKS");
					properties.setProperty("javax.net.ssl.trustStorePassword", "changeit");
				}
				conn = driver.connect(url, properties);
				if (conn.isValid(5)) {
					isValid = true;
				}
			} catch (Exception ex) {
				logger.error("connect oracle database has encountered Exception message {}, cause {}", ex.getMessage(),
						ex.getCause());

			} catch (Error error) {
				logger.error("connect oracle database has encountered Error message  {}, cause {}", error.getMessage(),
						error.getCause());

			} finally {
				// delete generate jks file in last
				CertUtil.deleteQuietlyJksFile(new File(SS_CONSTANT.JDBC_CONF_CRT_PATH));
				if (conn != null) {
					try {
						conn.close();
					} catch (SQLException e) {

					}
				}
			}
		}

		return isValid;
	}

	private String buildOracleConnUrl(DataBaseDTO dbConnInfo) {
		if (SS_CONSTANT.ORC_WAY_CONSTR.equals(dbConnInfo.getParam().getDb_orcway())) {
			String connStr = dbConnInfo.getParam().getDb_connectionString();
			return SS_CONSTANT.ORACLE_JDBC_URL_PREFIX + connStr;
		} else {
			return String.format(
					"jdbc:oracle:thin:@(DESCRIPTION=" + "(ADDRESS=(PROTOCOL=%s)(HOST=%s)(PORT=%s))"
							+ "(CONNECT_DATA=(SID=%s)))",
					jdbcUrlIsSecure(dbConnInfo), dbConnInfo.getParam().getDb_host(), dbConnInfo.getParam().getDb_port(),
					dbConnInfo.getParam().getDb_name());
		}
	}

	private String buildPostgresConnUrl(DataBaseDTO dataBaseDTO) {
		String tmp = String.format(SS_CONSTANT.POSTGRES_JDBC_URL, dataBaseDTO.getParam().getDb_host(),
				dataBaseDTO.getParam().getDb_port(), dataBaseDTO.getParam().getDb_name());
		return tmp;
	}

	private String jdbcUrlIsSecure(DataBaseDTO dbConnInfo) {
		return dbConnInfo.getParam().isSecure() ? "tcps" : "tcp";
	}

	private void remoteAllPreviousFiles(String type) throws IOException {
		File directory = new File(SS_CONSTANT.JDBC_CONF_CRT_PATH);
		if (directory.exists() && directory.isDirectory()) {
			Arrays.stream(directory.listFiles()).filter(file -> file.getName().startsWith(type))
					.forEach(LambdaExceptionUtil.rethrowConsumer(file -> file.delete()));
		}
	}

	private void uploadCertificates(String type, MultipartFile[] multipartFiles) throws IOException {
		Arrays.stream(multipartFiles).forEach(LambdaExceptionUtil.rethrowConsumer(multipartFile -> {
			File tmpFile = new File(
					SS_CONSTANT.JDBC_CONF_CRT_PATH + File.separator + type + "-" + multipartFile.getOriginalFilename());
			StreamUtils.writeByteArrayToFile(tmpFile, multipartFile.getBytes());
		}));
	}

	private Driver loadDriverFromExternalJar(URL[] urls) throws Exception {
		URLClassLoader urlClassLoader = new URLClassLoader(urls, System.class.getClassLoader());
		Class driverClass = urlClassLoader.loadClass(SS_CONSTANT.ORACLE_DRIVER);
		return (Driver) driverClass.newInstance();
	}

}
